Proyecto¶

Equipo:¶

  • Integrante 1: Rodrigo Mella S.
  • Integrante 2: Miguel Morales M.

  • Cuenta de la competencia: Rodrigo_Mella

  • Nombre del equipo: Samps

Link de repositorio de GitHub: <https://github.com/migmoralesmar/MDS7202¶

Codalab Competencia¶

  1. Clasificación de potenciales evaluaciones con las que los consumidores evaluarán las películas. Las posibles clases que deben asignar a cada juego son ('Negative', 'Mixed', 'Mostly Positive', 'Positive', 'Very Positive'). La métrica de evaluación utilizada para medir la clasificación es f1_macro.
  2. Regresión de los potenciales ingresos que tendrán las películas. La métrica de evaluación utilizada para medir la clasificación es r_2.

1. Introducción¶

El objetivo de este proyecto consiste en resolver dos problemas distintos asociados al mundo del cine, los cuales servirán para determinar el éxito o fracaso de una película. El primer problema consiste en implementar un modelo de clasificación capaz de asignar potenciales evaluaciones a las películas en distintos niveles de aceptación. Por otro lado, el segundo problema busca poder estimar el posible ingreso que podría generar cada película por medio de un modelo de regresión.

Los datos que se proveen, para poder llevar a cabo lo recién mencionado, corresponden a dos datasets con un total de 9641 ejemplos cada uno, los cuales presentan atributos numéricos y categóricos correspondientes a caracterísitcas particulares de cada película. Al juntar ambos datasets en función de los id se llegan a tener un total de 21 atributos, de los cuales se eliminaron 7 dado que algunos se repetían y otros presentaban información redundante e innecesario. Además, se realizó un procesamiento de la mayoría de las variables. De los 14 atributos restantes, 2 de ellos representan las variables objetivos de los modelos, las cuales son label, que es de tipo categórica y se utiliza para el problema de clasificación, y target que representa el ingreso de la película, es de tipo numérica y se usa para el modelo de regresión.

Con respecto a la evaluación del rendimiento de los modelos, se tiene que el de clasificación se evalua en base a la métrica f1_macro, ya que esta permite medir de forma simultánea las métricas de precisión y recall aplicadas sobre las predicciones realizadas por el modelo para un problema multiclase, lo cual sirve para determinar de mejor manera el desempeño cuando se tienen datos desbalanceados (como es el caso del problema de clasificación). Por otro lado, la tarea de regresión se evalúa por medio de la métrica R2 score, que permite determinar la calidad del modelo para replicar los resultados, y la proporción de variación de los resultados que puede explicarse por el modelo.

Nuestra propuesta para resolver el problema consistió en realizar un EDA inicial, con el fin de determinar características importantes de los datos que posteriormente se pudieran usar para el entrenamiento de los modelos. Una vez obtenida la información de los datos, se prosiguió a generar una función de features propia que nos permitiera extraer las caracterísitcas más útiles para realizar la clasificación y regresión. Con respecto a la selección de modelos, para la primera tarea se utilizaron en primer lugar algunos algoritmos básicos como clasificadores Dummy, DecisionTree, Random Forest y SVC, los cuales fueron tuneados mediante un GridSearch. Dado que se obtuvieron rendimiento bajos, se recurrió al uso de clasificadores XGBoost y LightGBM, los cuales también fueron tuneados vía GridSearch. Por otro lado, para la tarea de regresión, se siguió un procedimiento similar, pues se testeo primero la resolución del problema con los regresores básicos Dummy y DecisionTree. Sin embargo, puesto que el rendimiento de modelos XGBoost y LightGBM sobrepasaba a los clásicos en la tarea de clasificación, se decidió realizar un GridSearch únicamente para regresores de este tipo.

Para ambos problemas se obtuvo un mejor rendimiento con los modelos de XGBoost, los cuales fueron capaces de cumplir los objetivos del proyecto. Si bien los resultados obtenidos con un conjunto de validación no alcanzaron un rendimiento óptimo, siendo 0.33 el f1 score en clasificación y 0.566 R2 score en regresión, los resultados con el conjunto de test para la competencia fueron más que satisfactorios, dado que se obtuvieron scores de 0.96 y 0.89 para f1 y R2 respectivamente.

2. Preparación y Análisis Exploratorio de Datos¶

In [1]:
## Imports 

import numpy as np
import pandas as pd
from sklearn.pipeline import Pipeline

from sklearn.model_selection import train_test_split 

# Pre-procesamiento
from sklearn.feature_selection import SelectPercentile, f_classif, f_regression
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler,StandardScaler
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_extraction.text import CountVectorizer

# Clasifación y regresión
from sklearn.dummy import DummyClassifier, DummyRegressor
from sklearn.svm import SVC, SVR
from sklearn.naive_bayes import MultinomialNB
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression, ElasticNet, Ridge

# Metricas de evaluación
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import cohen_kappa_score
from sklearn.metrics import r2_score

# Librería para plotear
# !pip install --upgrade plotly
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go

# Proyecciones en baja dimensionalidad: UMAP
# !pip install umap-learn
import umap

# Librería para NLP
# !pip install nltk
import nltk
from nltk.corpus import stopwords
from nltk import word_tokenize  
from nltk.stem import PorterStemmer
nltk.download('stopwords')
nltk.download('punkt')

# Librerias modelos boosting 
# !pip install xgboost
# !pip install lightgbm
from xgboost import XGBClassifier, XGBRegressor
from lightgbm import LGBMClassifier, LGBMRegressor

import seaborn as sns
import matplotlib.pyplot as plt
import missingno as msno
import time
# import warnings

# warnings.filterwarnings("ignore")
[nltk_data] Downloading package stopwords to
[nltk_data]     C:\Users\tacom\AppData\Roaming\nltk_data...
[nltk_data]   Package stopwords is already up-to-date!
[nltk_data] Downloading package punkt to
[nltk_data]     C:\Users\tacom\AppData\Roaming\nltk_data...
[nltk_data]   Package punkt is already up-to-date!

Preparación de datos¶

In [2]:
##  Código Preparación de Datos.

## Load of data
num_data = pd.read_parquet('train_numerical_features.parquet', engine='pyarrow')
text_data = pd.read_parquet('train_text_features.parquet', engine='pyarrow')
full_data = num_data.merge(text_data,how='inner',on='id',suffixes=('','_y'))

print('Load of data')
print(list(num_data.columns),'\n',num_data.shape)
print(list(text_data.columns),'\n',text_data.shape)
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Eliminando columnas 
columns2drop = ['title_y','tagline_y','credits_y','poster_path', 'backdrop_path', 'recommendations']

full_data = full_data.drop(columns=columns2drop)
print('Eliminando columnas')
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Filtrar por revenue igual a cero
full_data = full_data.drop(full_data[full_data['revenue']==0].index,axis=0)

print('Filtrar por revenue igual a cero')
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Filtrar por release_date y runtime nulos
full_data = full_data.drop(full_data[(full_data['release_date'].isnull())|(full_data['runtime'].isnull())].index,axis=0)

print('Filtrar por release_date y runtime nulos')
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Cambiando columna realease_date a datetime
full_data['release_date'] = pd.to_datetime(full_data['release_date'].astype(str),format="%Y/%m/%d")

## Eliminando status distintos a released 
full_data = full_data.drop(full_data[full_data['status']!='Released'].index,axis=0)

print('Eliminando status distintos a released')
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Rellenar valores nulos categóricos y de texto
full_data.fillna('',axis=0,inplace=True)

## Generando labels 
bins = [0, 5, 6, 7, 8, 10]
labels = ['Negative','Mixed','Mostly Positive','Positive','Very Positive']
full_data['label'] = pd.cut(full_data['vote_average'], bins=bins, labels=labels)

## Eliminando columnas vote_average y id
full_data = full_data.drop(columns=['vote_average','id'])

print('Eliminando columnas vote_average y id')
print(list(full_data.columns),'\n',full_data.shape,'\n')

## Renombrando revenue como target
full_data.rename(columns={'revenue':'target'},inplace=True)

print('Renombrando revenue como target')
print(list(full_data.columns),'\n',full_data.shape,'\n')
Load of data
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'poster_path', 'backdrop_path', 'recommendations'] 
 (9641, 11)
['id', 'title', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'tagline', 'credits', 'keywords', 'vote_average'] 
 (9641, 11)
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'poster_path', 'backdrop_path', 'recommendations', 'title_y', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'tagline_y', 'credits_y', 'keywords', 'vote_average'] 
 (9641, 21) 

Eliminando columnas
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] 
 (9641, 15) 

Filtrar por revenue igual a cero
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] 
 (6451, 15) 

Filtrar por release_date y runtime nulos
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] 
 (6451, 15) 

Eliminando status distintos a released
['id', 'title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'vote_average'] 
 (6451, 15) 

Eliminando columnas vote_average y id
['title', 'budget', 'revenue', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'label'] 
 (6451, 14) 

Renombrando revenue como target
['title', 'budget', 'target', 'runtime', 'status', 'tagline', 'credits', 'genres', 'original_language', 'overview', 'production_companies', 'release_date', 'keywords', 'label'] 
 (6451, 14) 

EDA¶

Data type¶

In [3]:
## Código EDA
## Viendo types de columnas
full_data.dtypes
Out[3]:
title                           object
budget                         float64
target                         float64
runtime                        float64
status                          object
tagline                         object
credits                         object
genres                          object
original_language               object
overview                        object
production_companies            object
release_date            datetime64[ns]
keywords                        object
label                         category
dtype: object

Histogramas variables númericas y categóricas¶

In [4]:
full_data[['budget','target','runtime']].hist(bins=89,figsize=(15,7))
Out[4]:
array([[<AxesSubplot:title={'center':'budget'}>,
        <AxesSubplot:title={'center':'target'}>],
       [<AxesSubplot:title={'center':'runtime'}>, <AxesSubplot:>]],
      dtype=object)
In [5]:
fig = px.histogram(full_data, x="label",width=800, height=400,title='Histograma de Label')
fig.show()
fig = px.histogram(full_data, x="original_language",width=800, height=400,title='Histograma de Lenguaje original')
fig.show()

Scatter Matrix¶

In [7]:
pd.plotting.scatter_matrix(full_data,alpha=0.9,figsize=(15,10))
Out[7]:
array([[<AxesSubplot:xlabel='budget', ylabel='budget'>,
        <AxesSubplot:xlabel='target', ylabel='budget'>,
        <AxesSubplot:xlabel='runtime', ylabel='budget'>],
       [<AxesSubplot:xlabel='budget', ylabel='target'>,
        <AxesSubplot:xlabel='target', ylabel='target'>,
        <AxesSubplot:xlabel='runtime', ylabel='target'>],
       [<AxesSubplot:xlabel='budget', ylabel='runtime'>,
        <AxesSubplot:xlabel='target', ylabel='runtime'>,
        <AxesSubplot:xlabel='runtime', ylabel='runtime'>]], dtype=object)

Matriz de correlación¶

In [8]:
worthy_columns = ['budget','target','runtime','genres','original_language','label']
corr_matrix = full_data[worthy_columns].apply(lambda x : pd.factorize(x)[0]).corr(method='pearson', min_periods=1)

sns.set(rc = {'figure.figsize':(15,8)})

ax = sns.heatmap(
    corr_matrix, 
    vmin=-1, vmax=1, center=0,
    cmap=sns.diverging_palette(20, 220, n=200),
    square=True
)
ax.set_xticklabels(
    ax.get_xticklabels(),
    rotation=45,
    horizontalalignment='right'
)
Out[8]:
[Text(0.5, 0, 'budget'),
 Text(1.5, 0, 'target'),
 Text(2.5, 0, 'runtime'),
 Text(3.5, 0, 'genres'),
 Text(4.5, 0, 'original_language'),
 Text(5.5, 0, 'label')]

Tablas de contingencia¶

In [9]:
tabla = pd.crosstab(
    index=full_data["label"],
    columns=full_data["original_language"],
)

tabla
Out[9]:
original_language ab af ar bn cn da de el en es ... no pl pt ro ru sv te th tr zh
label
Negative 0 0 0 0 0 0 0 0 157 0 ... 0 0 0 0 1 0 0 0 0 0
Mixed 1 0 0 0 1 1 2 0 1259 7 ... 1 0 1 0 2 0 0 1 1 1
Mostly Positive 0 1 0 0 16 6 11 0 2682 28 ... 5 1 2 1 12 7 0 1 0 14
Positive 0 0 1 0 12 7 24 2 1373 47 ... 3 2 6 1 9 10 2 4 4 18
Very Positive 0 0 1 1 1 0 1 0 116 3 ... 0 0 5 0 2 0 0 0 1 1

5 rows × 34 columns

Valores faltantes¶

In [10]:
fig, ax = plt.subplots(figsize=[15, 10])

msno.matrix(full_data, ax=ax, sparkline=False)
Out[10]:
<AxesSubplot:>

UMAP¶

In [12]:
reducer = umap.UMAP(metric='chebyshev',random_state=4)

worthy_columns = ['budget','target','runtime']#,'genres','original_language']
umap_train = full_data[worthy_columns].values#.apply(lambda x : pd.factorize(x)[0]).values
scaled_umap_train = MinMaxScaler().fit_transform(umap_train)

embedding = reducer.fit_transform(scaled_umap_train)
embedding.shape
Out[12]:
(6451, 2)
In [13]:
map_dict = {'Negative':0,'Mixed':1,'Mostly Positive':2,'Positive':3,'Very Positive':4}
plt.scatter(
    embedding[:, 0],
    embedding[:, 1],
    c=[sns.color_palette()[x] for x in full_data.label.map(map_dict)])
plt.gca().set_aspect('equal', 'datalim')
plt.title('UMAP projection of the Movie Dataset', fontsize=24)
Out[13]:
Text(0.5, 1.0, 'UMAP projection of the Movie Dataset')

Scatter budget vs. target¶

In [6]:
fig = px.scatter(full_data,x='budget', y='target',title='Scatterplot Budget vs. Target')
fig.show()
In [7]:
sub_data = full_data.loc[full_data.production_companies.str.contains('Marvel', regex=False),:]
fig = px.scatter(sub_data,x='budget', y='target',title='Scatterplot Budget vs. Target para películas de Marvel',hover_name='title')
fig.show()
In [8]:
sub_data = full_data.loc[full_data.production_companies.str.contains('Warner', regex=False),:]
fig = px.scatter(sub_data,x='budget', y='target',title='Scatterplot Budget vs. Target para películas de Warner',hover_name='title')

fig.show()

Top 50 productoras¶

In [9]:
sub_data = full_data['production_companies'].apply(lambda x: x.split('-'))

list_productoras = pd.Series(sum(list(sub_data.values), []))
productoras = pd.DataFrame({'productoras':list_productoras,'counts':np.zeros(len(list_productoras))})

productoras_data = productoras.groupby('productoras',as_index=False).count()
productoras_data = productoras_data.sort_values(by='counts',ascending=False)[:50]
top_productoras = productoras_data.values
fig = px.bar(productoras_data, x="productoras",y='counts',width=900, height=800,title='Top 50 productoras')
fig.show()

Top 50 artistas¶

In [10]:
sub_data = full_data['credits'].apply(lambda x: x.split('-'))

list_actors = pd.Series(sum(list(sub_data.values), []))
actors = pd.DataFrame({'actores':list_actors,'counts':np.zeros(len(list_actors))})

actors_data = actors.groupby('actores',as_index=False).count()
# Se descartan los nombres que probablemente correspondían a un nombre compuesto
actors_data['nchars'] = actors_data.actores.apply(lambda x: len(x.split(' ')))
actors_data = actors_data.loc[actors_data.nchars>1,:]
actors_data = actors_data.sort_values(by='counts',ascending=False)[:50]
top_artistas = actors_data.values
fig = px.bar(actors_data, x="actores",y='counts',width=900, height=800,title='Top 50 artistas')
fig.show()

Top 50 keywords¶

In [11]:
sub_data = full_data['keywords'].apply(lambda x: x.split('-'))

list_keywords = pd.Series(sum(list(sub_data.values), []))
keywords = pd.DataFrame({'keywords':list_keywords,'counts':np.zeros(len(list_keywords))})

keywords_data = keywords.groupby('keywords',as_index=False).count()
keywords_data = keywords_data.sort_values(by='counts',ascending=False)[:50]
top_keywords = keywords_data.values
fig = px.bar(keywords_data, x="keywords",y='counts',width=900, height=800,title='Top 50 keywords')
fig.show()

Top genres¶

In [12]:
sub_data = full_data['genres'].apply(lambda x: x.split('-'))

list_genres = pd.Series(sum(list(sub_data.values), []))
genres = pd.DataFrame({'genres':list_genres,'counts':np.zeros(len(list_genres))})

genres_data = genres.groupby('genres',as_index=False).count()
genres_data = genres_data.sort_values(by='counts',ascending=False)[:50]
top_genres = genres_data.values
fig = px.bar(genres_data, x="genres",y='counts',width=900, height=800,title='Top 50 genres')
fig.show()

Análisis del EDA

En primera instancia, es necesario verificar y corregir cierta información errónea o no útil dentro de la base de datos, es por esto que se procede a eliminar las columnas de 'title_y','tagline_y','credits_y','poster_path', 'backdrop_path', 'recommendations', además, se filtran y mantienen las columnas con fechas y recaudación no nulas, finalmente, se genera una categorización de las notas de usuarios por cinco categorías más generales y se designan como labels del dataset de clasificación.

Respecto a las nuevas labels categóricas, se puede apreciar un gran desbalance de clases, donde la clase 'Mostly Positive' contiene 2983 entradas, mientras que las clases 'Very Positive' y 'Negative' tan sólo contienen 185 y 175 respectivamente. Esto afectará en gran medida el desempeño de los clasificadores, principalmente por el sobre-ajuste a las clases más predominantes. Para monitorear esto será sumamente importante observar el comportamiento de la métrica Recall o F1 para las clases menos predominantes. Para la presencia de idiomas, es notable que el idioma inglés predomina por sobre el resto.

En la dispersión de datos según dos variables, es decir, en un scatter plot, se puede identificar grandes acumulaciones de datos para valores bajos de budget con target, y ciertos valores alejados de la tendencia. Algo similar se puede osbservar al relacionar las variables de target y budget con la duración de la película, donde la mayor parte de los datos se observa alrededor del intervalo 100-150 minutos. Para la correlación de datos, en la matriz de correlación se puede observar que las variables con mayor correlación positiva respecto a target son genres, budget y original_language, donde esta última ya se pudo ver con bastante desbalance.

Al generar una reducción de dimensionalidad y scatter plot con UMAP, con las variables target, budget y runtime, se puede observar que no existen clusters muy identificables, por lo que será necesario un procesamiento de las variables no numéricas para complementar la caracterización de los conjuntos o labels.

Finalmente, respecto a la presencia de entidades importantes, tanto para las productoras, artistas, keywords y géneros se hizo una limpieza de datos no representativos o mal registrados, además de una separación por carácteres (por ejemplo: '-') para diferenciar entre entidades individuales y no agrupaciones. Es con esto que se puede observar la presencia de grandes productoras con presencia en la gran mayoría de películas registradas pero también una gran variedad de otras que son pseudónimos de la misma empresa pero que recaen en otra categoría (por ejemplo: Marvel y Walt Disney Pictures). Para los artistas, existían varios cuyos nombres no tenían identificación por apellido o al menos no fue posible rescatar de forma automática, por lo que no son considerados dentro del top 50, de forma similar ocurrió para las keywords y destacar que los top géneros de película son en categorías singulares y no compuestas, con la mayor presencia de películas de 'Drama'.


3. Preprocesamiento, Holdout y Feature Engineering¶

In [10]:
## Código Holdout
from sklearn.preprocessing import LabelEncoder

# Quitando la columna label de la base de datos a utilizar
X = full_data.drop(columns=['label','title'])
y = full_data['label']

X_train, X_test, ylabel_train, ylabel_test = train_test_split(X,y, stratify=y, test_size=0.2,shuffle=True,random_state=7)

# Encoder numérico para las labels
label_encoder = LabelEncoder()
label_encoder = label_encoder.fit(ylabel_train)
ylabel_train = label_encoder.transform(ylabel_train)
ylabel_test = label_encoder.transform(ylabel_test)

ytarget_train = X_train['target']
ytarget_test = X_test['target']

X_train.drop(columns=['target'],inplace=True)
X_test.drop(columns=['target'],inplace=True)

# Creando dataset completo para entrenar los mejores pipelines
X_full = X.drop(columns='target')
y_full_label = label_encoder.transform(y)
y_full_target = X.target
In [11]:
# Función para obtener la temporada del año según el release date
def season_of_date(date):
    year = str(date.year)
    seasons = {'spring': pd.date_range(start=year+'/03/21', end=year+'/06/20'),
               'summer': pd.date_range(start=year+'/06/21', end=year+'/09/22'),
               'autumn': pd.date_range(start=year+'/09/23', end=year+'/12/20')}
    if date in seasons['spring']:
        return 'spring'
    if date in seasons['summer']:
        return 'summer'
    if date in seasons['autumn']:
        return 'autumn'
    else:
        return 'winter'
In [12]:
## Código Feature Engineering (Opcional)
def custom_features(df,top_artistas=top_artistas,top_productoras=top_productoras,top_keywords=top_keywords):
    df_copy = df.copy()
    # Codeando fecha
    df_copy['release_month'] = df_copy['release_date'].dt.month
    df_copy['season'] = df_copy.release_date.map(season_of_date)

    # Contando personajes celebres por pelicula
    df_copy['n_celebrities'] = df_copy['credits'].apply(lambda x: len(set(x.split('-')) & set(top_artistas[:,0])))
    df_copy['celebrities'] = df_copy['credits'].apply(lambda x: list((set(x.split('-')) & set(top_artistas[:,0]))) 
                                                    if len(set(x.split('-')) & set(top_artistas[:,0]))>=1 else [])

    # Contando personajes celebres por pelicula
    df_copy['in_top_production_companies'] = df_copy['production_companies'].apply(lambda x: 1 if (len(set(x.split('-')) & set(top_productoras[:,0])))>=1 else 0)

    # Categorizacion de keywords
    df_copy['top_keyword'] = df_copy['keywords'].apply(lambda x: x.split('-')[0]
                                                    if (x.split('-')[0] in top_keywords[:,0]) else 'other')

    # Ratios
    df_copy['runtime_budget_ratio'] = 0
    indexes = df_copy['budget']!=0
    df_copy.loc[indexes,'runtime_budget_ratio'] = df_copy.loc[indexes,'runtime']/df_copy.loc[indexes,'budget']

    # Conteo actores, productoras y generos
    df_copy['n_actors'] = df_copy['credits'].apply(lambda x: len(x.split('-')))
    df_copy['n_production_companies'] = df_copy['production_companies'].apply(lambda x: len(x.split('-')))
    df_copy['n_genres'] = df_copy['genres'].apply(lambda x: len(x.split('-')))

    # Top genre + keyword
    df_copy['genre_keyword'] = df_copy['genres'].apply(lambda x: (x.split('-'))[0]) + '-' + df_copy['top_keyword']

    return df_copy
In [13]:
custom_data = custom_features(full_data)
In [26]:
custom_data.hist(figsize=(15,11))
Out[26]:
array([[<AxesSubplot:title={'center':'budget'}>,
        <AxesSubplot:title={'center':'target'}>,
        <AxesSubplot:title={'center':'runtime'}>],
       [<AxesSubplot:title={'center':'release_date'}>,
        <AxesSubplot:title={'center':'release_month'}>,
        <AxesSubplot:title={'center':'n_celebrities'}>],
       [<AxesSubplot:title={'center':'in_top_production_companies'}>,
        <AxesSubplot:title={'center':'runtime_budget_ratio'}>,
        <AxesSubplot:title={'center':'n_actors'}>],
       [<AxesSubplot:title={'center':'n_production_companies'}>,
        <AxesSubplot:title={'center':'n_genres'}>, <AxesSubplot:>]],
      dtype=object)
In [27]:
custom_data.dtypes
Out[27]:
title                                  object
budget                                float64
target                                float64
runtime                               float64
status                                 object
tagline                                object
credits                                object
genres                                 object
original_language                      object
overview                               object
production_companies                   object
release_date                   datetime64[ns]
keywords                               object
label                                category
release_month                           int64
season                                 object
n_celebrities                           int64
celebrities                            object
in_top_production_companies             int64
top_keyword                            object
runtime_budget_ratio                  float64
n_actors                                int64
n_production_companies                  int64
n_genres                                int64
genre_keyword                          object
dtype: object
In [14]:
# Generamos tokenizador
stop_words = stopwords.words('english')

# Definimos un tokenizador con Stemming
class StemmerTokenizer:
    def __init__(self):
        self.ps = PorterStemmer()
    def __call__(self, doc):
        doc_tok = word_tokenize(doc)
        doc_tok = [t for t in doc_tok if t not in stop_words]
        return [self.ps.stem(t) for t in doc_tok]
In [15]:
## Código ColumnTransformer

coltransf = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_uni_tri_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
    ('BoW_uni_tri_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])

Comentarios sobre preparación de datos

Se construye un holdout para el dataset original, tomando en consideración el no utilizar las columnas target y labels para el entrenamiento, y almacenándolas como ground-truth labels para cada task que se trabajará posteriormente.

Se decidió por la implementación de un Custom Feature Engineering, donde éste tiene la presencia de features adicionales con relación a: mes de publicación, estación del año de publicación, cantidad de artistas del top 50, artistas del top 50, presencia de productora top 50, primera keyword presente (o más representativa), couciente entre duración de película y budget, cantidad de actores, cantidad de productoras, cantidad de géneros, categoría de género-keyword con las primeras presentes de cada una (ejemplo: Action-Vampire). De esta forma al aplicar sobre el dataset previamente limpio, se obtienen un total de 25 features previo a la implementación de un column transformer.

Para la implementación de un column transformer se tomaron en consideración las siguientes aplicaciones: MinMaxScaler, StandardScaler, One-Hot Encoder, Ordinal Encoder, y CountVectorizer mediante Bag of Words en n_gramas para las features de overview y tagline. Cada una de estas aplicaciones fue realizada según criterio de grupo y para toda feature que no fue transformada, ésta quedará fuera de la base de entrenamiento.


4. Clasificación¶

4.1 Dummy y Baseline¶

In [16]:
target_names = full_data['label'].unique()
In [17]:
def evaluation_cls(pipeline,X_train,y_train,X_test,y_test,target_names,classifier_name):
    print(f'Pipeline: {classifier_name}')
    pipeline.fit(X_train, y_train)
    y_pred = pipeline.predict(X_test)
    print(classification_report(y_test, y_pred, target_names=target_names,zero_division=0),'\n')
    f1 = round(f1_score(y_test, y_pred,average='macro'), 3)
    print("F1 Score:", f1, end='\t')
    kappa = round(cohen_kappa_score(y_test, y_pred), 3)
    print("Kappa:", kappa, end='\t')
    accuracy = round(accuracy_score(y_test, y_pred), 3)
    print("Accuracy:", accuracy, '\n \n')
In [18]:
## Código Dummy
pipe_dummy_cls = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_classif, percentile=40)),
                ('clf',DummyClassifier(strategy="stratified",random_state=7)),
                ])
In [19]:
## Código Clasificador
pipe_dTree_cls = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_classif, percentile=40)),
                ('clf',DecisionTreeClassifier(criterion="entropy",random_state=7)),
                ])
In [20]:
## Código Comparación de métricas
evaluation_cls(pipe_dummy_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DummyClassifier')
evaluation_cls(pipe_dTree_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DecisionTreeClassifier')
Pipeline: DummyClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.23      0.21      0.22       268
       Positive       0.49      0.50      0.50       597
  Very Positive       0.00      0.00      0.00        35
          Mixed       0.29      0.29      0.29       354
       Negative       0.02      0.03      0.02        37

       accuracy                           0.36      1291
      macro avg       0.21      0.21      0.21      1291
   weighted avg       0.35      0.36      0.36      1291
 

F1 Score: 0.206	Kappa: 0.031	Accuracy: 0.356 
 

Pipeline: DecisionTreeClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.29      0.26      0.28       268
       Positive       0.49      0.53      0.51       597
  Very Positive       0.12      0.09      0.10        35
          Mixed       0.43      0.45      0.44       354
       Negative       0.25      0.08      0.12        37

       accuracy                           0.43      1291
      macro avg       0.32      0.28      0.29      1291
   weighted avg       0.42      0.43      0.42      1291
 

F1 Score: 0.29	Kappa: 0.12	Accuracy: 0.427 
 

Se puede observar como para un modelo básico como Decision Tree inicializado de forma automática, sin tunning de hiperparámetros, se llega a obtener un mejor resultado que un modelo Dummy, pero de todas formas será necesaria la implementación de gridsearch y elección de múltiples modelos, tanto simples como más complejos, para mejorar este desempeño.


4.2 Búsqueda del mejor modelo de Clasificación¶

Modelos clásicos¶

In [150]:
### Código GridSearch Modelos Tipicos 

coltransf_grid_base_cls = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
    ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])

pipe_grid_base_cls = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_grid_base_cls),
        ("selection", SelectPercentile(f_classif)),
        ("model", SVC()),
    ]
)


grid_base_cls = [
    # grilla 1: SVC
    {   "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3),(2,3)],
        "selection__percentile": [40,60,80],
        "model": [SVC(random_state=7)],
        "model__kernel": ["rbf", "linear"],
        "model__C": [1,10]
    },
    # grilla 2: Random Forest
    {   "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3),(2,3)],
        "selection__percentile": [40,60,80],
        "model": [RandomForestClassifier(random_state=7)],
        "model__n_estimators": [30,100],
        "model__max_depth": [15,None]
    },
    # grilla 3: Decision Tree
    {   "preprocessing__BoW_overview__ngram_range": [(1,1),(1,2),(2,3)],
        "selection__percentile": [40,60,80],
        "model": [DecisionTreeClassifier(random_state=7)],
        "model__criterion": ['gini','entropy','log_loss'],
        "model__max_depth": [15,None]
    },
]

gs = GridSearchCV(pipe_grid_base_cls, grid_base_cls, n_jobs=-1, scoring="f1_macro",verbose=10).fit(X_train, ylabel_train)
In [161]:
gs.best_score_
Out[161]:
0.29898709726015393
In [152]:
gs.best_estimator_
Out[152]:
Pipeline(steps=[('CustomFeatures',
                 FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
                ('preprocessing',
                 ColumnTransformer(transformers=[('MinMaxScaler',
                                                  MinMaxScaler(),
                                                  ['budget', 'n_celebrities',
                                                   'runtime_budget_ratio',
                                                   'n_actors',
                                                   'n_production_companies']),
                                                 ('StandardScaler',
                                                  StandardScaler(),
                                                  ['runtime', 'n_genres']),
                                                 ('OneHot',
                                                  OneHotE...
                                                 ('BoW_overview',
                                                  CountVectorizer(tokenizer=<__main__.StemmerTokenizer object at 0x0000022403E55D90>),
                                                  'overview'),
                                                 ('BoW_tagline',
                                                  CountVectorizer(ngram_range=(1,
                                                                               3),
                                                                  tokenizer=<__main__.StemmerTokenizer object at 0x0000022403E55F40>),
                                                  'tagline')])),
                ('selection', SelectPercentile(percentile=60)),
                ('model',
                 DecisionTreeClassifier(criterion='entropy', random_state=7))])
In [169]:
coltransf_best_base_cls = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,1)),'overview'),
    ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])

pipe_best_base_cls = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best_base_cls),
        ("selection", SelectPercentile(f_classif,percentile=60)),
        ("model", DecisionTreeClassifier(criterion='entropy', random_state=7)),
    ]
)
evaluation_cls(pipe_best_base_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'DecisionTreeClassifier')
Pipeline: DecisionTreeClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.31      0.30      0.30       268
       Positive       0.49      0.49      0.49       597
  Very Positive       0.08      0.06      0.07        35
          Mixed       0.38      0.41      0.39       354
       Negative       0.12      0.08      0.10        37

       accuracy                           0.40      1291
      macro avg       0.27      0.27      0.27      1291
   weighted avg       0.40      0.40      0.40      1291
 

F1 Score: 0.269	Kappa: 0.098	Accuracy: 0.403 
 

Modelos de Boosting¶

In [170]:
pipe_xgboost_cls = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_classif, percentile=90)),
                ('clf',XGBClassifier(random_state=7)),
                ])


pipe_lgbm_cls = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_classif, percentile=90)),
                ('clf',LGBMClassifier(random_state=7)),
                ])
Pipeline: LightGBMClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.41      0.26      0.32       268
       Positive       0.51      0.67      0.58       597
  Very Positive       0.00      0.00      0.00        35
          Mixed       0.52      0.48      0.50       354
       Negative       0.00      0.00      0.00        37

       accuracy                           0.50      1291
      macro avg       0.29      0.28      0.28      1291
   weighted avg       0.46      0.50      0.47      1291
 

F1 Score: 0.28	Kappa: 0.191	Accuracy: 0.497 
 

In [171]:
## Código Comparación de métricas
start_time = time.time()
evaluation_cls(pipe_xgboost_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'XGBoostClassifier')
evaluation_cls(pipe_lgbm_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'LightGBMClassifier')
print(f'Tiempo de duración: {time.time()-start_time}')
Pipeline: XGBoostClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.39      0.22      0.29       268
       Positive       0.51      0.71      0.59       597
  Very Positive       0.00      0.00      0.00        35
          Mixed       0.53      0.45      0.49       354
       Negative       0.67      0.05      0.10        37

       accuracy                           0.50      1291
      macro avg       0.42      0.29      0.29      1291
   weighted avg       0.48      0.50      0.47      1291
 

F1 Score: 0.292	Kappa: 0.181	Accuracy: 0.497 
 

Pipeline: LightGBMClassifier
[LightGBM] [Warning] Auto-choosing col-wise multi-threading, the overhead of testing was 0.017483 seconds.
You can set `force_col_wise=true` to remove the overhead.
[LightGBM] [Info] Total Bins 5337
[LightGBM] [Info] Number of data points in the train set: 5160, number of used features: 1647
[LightGBM] [Info] Start training from score -1.571411
[LightGBM] [Info] Start training from score -0.771318
[LightGBM] [Info] Start training from score -3.607049
[LightGBM] [Info] Start training from score -1.294514
[LightGBM] [Info] Start training from score -3.551480
                 precision    recall  f1-score   support

Mostly Positive       0.41      0.26      0.32       268
       Positive       0.51      0.67      0.58       597
  Very Positive       0.00      0.00      0.00        35
          Mixed       0.52      0.48      0.50       354
       Negative       0.00      0.00      0.00        37

       accuracy                           0.50      1291
      macro avg       0.29      0.28      0.28      1291
   weighted avg       0.46      0.50      0.47      1291
 

F1 Score: 0.28	Kappa: 0.191	Accuracy: 0.497 
 

Tiempo de duración: 38.90761113166809
In [326]:
### Código GridSearch Modelos XGBoost y LightGBM

coltransf_grid_boost = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month'])
])

pipe_grid_boost_cls = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_grid_boost),
        ("selection", SelectPercentile(f_classif, percentile=90)),
        ("model", LGBMClassifier(random_state=7)),
    ]
)


grid_boosting_cls = [
    # grilla 1: XGBoost
    {   "model": [XGBClassifier(random_state=7)],
        "model__subsample": [0.5, 1],
        "model__colsample_bytree": [0.5, 1],
        "model__learning_rate": [0.3,  0.03],
        "model__max_depth": [2, 12],
        "model__n_estimators": [100],
        "model__min_child_weight": [1,5,15],
    },
    # grilla 2: LightGBM
    {   "model": [LGBMClassifier(random_state=7)],
        "model__boosting_type": ['gbdt','dart'],
        "model__n_estimators": [8,24],
        "model__learning_rate": [0.005, 0.01],
        "model__colsample_bytree": [0.64, 0.66],
        "model__subsample": [0.7,0.75]
    },
]

gs_boost_cls = GridSearchCV(pipe_grid_boost_cls, grid_boosting_cls, n_jobs=-1, scoring="f1_macro",verbose=10).fit(X_train, ylabel_train)
Fitting 5 folds for each of 80 candidates, totalling 400 fits
In [327]:
gs_boost_cls.best_score_
Out[327]:
0.3147265518100161
In [338]:
gs_boost_cls.best_params_
Out[338]:
{'model': XGBClassifier(base_score=None, booster=None, callbacks=None,
               colsample_bylevel=None, colsample_bynode=None, colsample_bytree=1,
               early_stopping_rounds=None, enable_categorical=False,
               eval_metric=None, gamma=None, gpu_id=None, grow_policy=None,
               importance_type=None, interaction_constraints=None,
               learning_rate=0.3, max_bin=None, max_cat_to_onehot=None,
               max_delta_step=None, max_depth=12, max_leaves=None,
               min_child_weight=5, missing=nan, monotone_constraints=None,
               n_estimators=100, n_jobs=None, num_parallel_tree=None,
               predictor=None, random_state=7, reg_alpha=None, reg_lambda=None, ...),
 'model__colsample_bytree': 1,
 'model__learning_rate': 0.3,
 'model__max_depth': 12,
 'model__min_child_weight': 5,
 'model__n_estimators': 100,
 'model__subsample': 1}
In [362]:
coltransf_best = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
#     ("Ordinal",OrdinalEncoder(),['release_month']),
#     ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
#     ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])

# release_month,'season',
best_pipe_cls = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best),
        ("selection", SelectPercentile(f_classif,percentile=65)),
        ("model", XGBClassifier(subsample=1, random_state=7,colsample_bytree=1,learning_rate=0.3,max_depth=12,
                               min_child_weight=5, n_estimators=100)),
    ]
)
evaluation_cls(best_pipe_cls,X_train,ylabel_train,X_test,ylabel_test,target_names,'XGBClassifier')
Pipeline: XGBClassifier
                 precision    recall  f1-score   support

Mostly Positive       0.38      0.30      0.33       268
       Positive       0.53      0.66      0.59       597
  Very Positive       0.43      0.09      0.14        35
          Mixed       0.52      0.47      0.50       354
       Negative       0.25      0.05      0.09        37

       accuracy                           0.50      1291
      macro avg       0.42      0.31      0.33      1291
   weighted avg       0.48      0.50      0.48      1291
 

F1 Score: 0.33	Kappa: 0.207	Accuracy: 0.5 
 

4.3 Entrenamiento con dataset completo para mejor pipeline de clasificación¶

In [363]:
coltransf_best = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','top_keyword',
                                                      'in_top_production_companies','genre_keyword'])
])

# release_month,'season'
best_pipe_cls = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best),
        ("selection", SelectPercentile(f_classif,percentile=65)),
        ("model", XGBClassifier(subsample=1, random_state=7,colsample_bytree=1,learning_rate=0.3,max_depth=12,
                               min_child_weight=5, n_estimators=100)),
    ]
)


best_pipe_cls.fit(X_full, y_full_label)
Out[363]:
Pipeline(steps=[('CustomFeatures',
                 FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
                ('preprocessing',
                 ColumnTransformer(transformers=[('MinMaxScaler',
                                                  MinMaxScaler(),
                                                  ['budget', 'n_celebrities',
                                                   'runtime_budget_ratio',
                                                   'n_actors',
                                                   'n_production_companies']),
                                                 ('StandardScaler',
                                                  StandardScaler(),
                                                  ['runtime', 'n_genres']),
                                                 ('OneHot',
                                                  OneHotE...
                               gamma=0, gpu_id=-1, grow_policy='depthwise',
                               importance_type=None, interaction_constraints='',
                               learning_rate=0.3, max_bin=256,
                               max_cat_to_onehot=4, max_delta_step=0,
                               max_depth=12, max_leaves=0, min_child_weight=5,
                               missing=nan, monotone_constraints='()',
                               n_estimators=100, n_jobs=0, num_parallel_tree=1,
                               objective='multi:softprob', predictor='auto',
                               random_state=7, reg_alpha=0, ...))])

Justificación de elección de clasificador

Se realizó un gridsearch para modelos tradicionales y simples, tales como DecisionTree, SVC y Random Forest. Con esto se obtuvo un modelo con un F1-score sobre el conjunto de entrenamiento de 0.298, esto con un clasificador compuesto de DecisionTree Classifier, todas las funciones definidas del column transformer, una selección de mejores 60% de features y la aplicación de custom features, sin embargo, el desempeño sobre el conjunto de test de prueba fue de 0.269.

Posteriormente se procedió a realizar un gridsearch pero esta vez con algoritmos de tipo boosting, en este caso XGBoosting y LightGBM. Para esta busqueda se mantuvieron los atributos y funciones utilizadas para la busqueda de un modelo más simple. De esta forma el algoritmo XGBoosting con sus parámetros encontrados obtuvo un F1-score de 0.314 sobre el conjunto de entrenamiento, es por esto que se decidió proceder a un fine-tunning de custom features con esta implementación.

Con el mejor modelo encontrado en un gridsearch de algoritmos de boosting, se procede a eliminar y/o modificar ciertas features que podrían perjudicar el desempeño. Tras múltiples experimentos y pruebas se llegó al mejor resultado mediante la eliminación de features relacionadas a fechas, en este caso 'release_month' y 'season', además se optó por remover de forma completa toda utilización de count-vectorizer o procesamiento sobre columnas de texto mediante Bag of Words. Finalmente se decide utilizar un select-percentil de features sobre el 65% de estas, lo que en conjunto entrega un F1-score de 0.33 sobre el conjunto de test.

Para efectos de predicción para la competencia, se decide realizar una base de datos más grande, mezclando los conjuntos previos de entrenamiento y test, sin considerar las columnas de 'label' o 'target' debido a su posible overfitting, de esta forma se aumenta la cantidad de instancias para entrenar. Se corroboró con la entrega de estas predicciones que se obtiene un mejor resultado con el conjunto de competencia que es independiente a los de prueba local.


5. Regresión¶

5.1 Dummy y Baseline¶

In [21]:
def evaluation_reg(pipeline,X_train,y_train,X_test,y_test,regressor_name):
    print(f'Pipeline: {regressor_name}')
    pipeline.fit(X_train, y_train)
    y_pred_reg = pipeline.predict(X_test)

    R2 = r2_score(y_test, y_pred_reg)
    print(f'Valor de R2: {R2}','\n')
In [22]:
## Código Dummy
pipe_dummy_reg = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_regression, percentile=90)),
                ('reg',DummyRegressor(strategy="mean")),
                ])
In [23]:
## Código Regresor
pipe_dTree_reg = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_regression, percentile=90)),
                ('reg',DecisionTreeRegressor(criterion="squared_error",random_state=7)),
                ])
In [24]:
## Código Comparación de métricas
evaluation_reg(pipe_dummy_reg,X_train,ytarget_train,X_test,ytarget_test,'DummyRegressor')
evaluation_reg(pipe_dTree_reg,X_train,ytarget_train,X_test,ytarget_test,'DecisionTreeRegressor')
Pipeline: DummyRegressor
Valor de R2: -0.0010585663913631471 

Pipeline: DecisionTreeRegressor
Valor de R2: 0.3052435078623271 

Claramente la utilización de regresión con un modelo simple como lo es Decision Tree, de forma automática, entrega mejor resultados de partida respecto a la métrica R2 en comparación con un regresor Dummy, de todas formas este nivel de desempeño es bajo y es necesario encontrar un conjunto de parámetros y modelos para optimizar.


5.2 Búsqueda del mejor modelo de Regresión¶

In [ ]:
### Código GridSearch

coltransf_grid_base_reg = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,1)),'overview'),
    ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])

pipe_grid_base_reg = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_grid_1),
        ("selection", SelectPercentile(f_regression)),
        ("model", SVR()),
    ]
)


grid_base_reg = [
    # grilla 1: SVR
    {   "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
        "selection__percentile": [40,60,80],
        "model": [SVR()],
        "model__kernel": ["rbf", "linear"],
        "model__C": [1,10]
    },
    # grilla 2: ElasticNet
    {   "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
        "selection__percentile": [40,60,80],
        "model": [ElasticNet(random_state=7)],
        "model__alpha": [1e-2, 1e-1, 1],
        "model__fit_intercept": [True, False],
        "model__max_iter": [100, 1000]
    },
    # grilla 3: DecisionTree Regressor
    {   "preprocessing__BoW_overview__ngram_range": [(1,2),(1,3)],
        "selection__percentile": [40,60,80],
        "model": [DecisionTreeRegressor(random_state=7)],
        "model__criterion": ['squared_error', 'friedman_mse', 'absolute_error'],
        "model__max_depth": [15,None]
    },
]

gs_base_reg = GridSearchCV(pipe_grid_base_reg, grid_base_reg, n_jobs=-1, scoring='r2',verbose=10).fit(X_train, ytarget_train)

Modelos de Boosting¶

In [233]:
pipe_xgboost_reg = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_regression, percentile=90)),
                ('reg',XGBRegressor(random_state=7)),
                ])


pipe_lgbm_reg = Pipeline([
                ('CustomFeatures',FunctionTransformer(custom_features)),
                ("Preprocess",coltransf),
                ("Selection", SelectPercentile(f_regression, percentile=90)),
                ('reg',LGBMRegressor(random_state=7)),
                ])
In [234]:
## Código Comparación de métricas
start_time = time.time()
evaluation_reg(pipe_xgboost_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBoostRegressor')
evaluation_reg(pipe_lgbm_reg,X_train,ytarget_train,X_test,ytarget_test,'LightGBMRegressor')
print(f'Tiempo de duración: {time.time()-start_time}')
Pipeline: XGBoostRegressor
Valor de R2: 0.528560233966219 

Pipeline: LightGBMRegressor
[LightGBM] [Warning] Auto-choosing row-wise multi-threading, the overhead of testing was 0.008478 seconds.
You can set `force_row_wise=true` to remove the overhead.
And if memory is not enough, you can set `force_col_wise=true`.
[LightGBM] [Info] Total Bins 5316
[LightGBM] [Info] Number of data points in the train set: 5160, number of used features: 1640
[LightGBM] [Info] Start training from score 91288722.518411
Valor de R2: 0.5421598692147556 

Tiempo de duración: 25.13885235786438
In [244]:
coltransf_grid_boost_reg = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
#     ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,2)),'overview'),
#     ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline'),
])

pipe_grid_boost_reg = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_grid_boost_reg),
        ("selection", SelectPercentile(f_regression,percentile=90)),
        ("model", LGBMRegressor(random_state=7)),
    ]
)


grid_boosting_reg = [
    # grilla 1: XGBoost
    {   "model": [XGBRegressor(random_state=7)],
        "model__learning_rate": [0.05, 0.10],
        "model__max_depth": [3, 8],
        "model__min_child_weight": [1, 7],
        "model__gamma": [0.0, 0.2],
        "model__colsample_bytree": [0.3, 0.4]
    },
    # grilla 2: LightGBM
    {   "model": [LGBMRegressor(random_state=7)],
        "model__num_leaves": [7, 50],
        "model__n_estimators": [50,200],
        "model__learning_rate": [0.1, 0.003],
        "model__max_depth": [-1, 5]
    },
]

gs_boost_reg = GridSearchCV(pipe_grid_boost_reg, grid_boosting_reg, n_jobs=-1, scoring="r2",verbose=10).fit(X_train, ytarget_train)
Fitting 5 folds for each of 48 candidates, totalling 240 fits
In [245]:
gs_boost_reg.best_score_
Out[245]:
0.5903709251984217
In [246]:
gs_boost_reg.best_estimator_[3]
Out[246]:
XGBRegressor(base_score=0.5, booster='gbtree', callbacks=None,
             colsample_bylevel=1, colsample_bynode=1, colsample_bytree=0.4,
             early_stopping_rounds=None, enable_categorical=False,
             eval_metric=None, gamma=0.0, gpu_id=-1, grow_policy='depthwise',
             importance_type=None, interaction_constraints='',
             learning_rate=0.05, max_bin=256, max_cat_to_onehot=4,
             max_delta_step=0, max_depth=8, max_leaves=0, min_child_weight=1,
             missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0,
             num_parallel_tree=1, predictor='auto', random_state=7, reg_alpha=0,
             reg_lambda=1, ...)
In [247]:
coltransf_best_boost_reg = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month'])
])

pipe_best_boost_reg = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best_boost_reg),
        ("selection", SelectPercentile(f_regression,percentile=90)),
        ("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
                               gamma=0.0,colsample_bytree=0.4)),
    ]
)


evaluation_reg(pipe_best_boost_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBRegressor')
Pipeline: XGBRegressor
Valor de R2: 0.5661824539456413 

In [26]:
coltransf_best_reg = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
    ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])

best_pipe_reg = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best_reg),
        ("selection", SelectPercentile(f_regression,percentile=90)),
        ("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
                               gamma=0.0,colsample_bytree=0.4)),
    ]
)


evaluation_reg(best_pipe_reg,X_train,ytarget_train,X_test,ytarget_test,'XGBRegressor')
Pipeline: XGBRegressor
Valor de R2: 0.5677415589552661 

5.3 Entrenamiento con dataset completo para mejor pipeline de regresión¶

In [322]:
coltransf_best_reg = ColumnTransformer([
    ("MinMaxScaler",MinMaxScaler(),['budget','n_celebrities','runtime_budget_ratio','n_actors',
                                    'n_production_companies']),
    ("StandardScaler",StandardScaler(),['runtime','n_genres']),
    ("OneHot",OneHotEncoder(handle_unknown='ignore'),['original_language','season','top_keyword',
                                                      'in_top_production_companies','genre_keyword']),
    ("Ordinal",OrdinalEncoder(),['release_month']),
    ('BoW_overview',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'overview'),
    ('BoW_tagline',CountVectorizer(tokenizer= StemmerTokenizer(),ngram_range=(1,3)),'tagline')
])


best_pipe_reg = Pipeline(
    [   ('CustomFeatures',FunctionTransformer(custom_features)),
        ("preprocessing", coltransf_best_reg),
        ("selection", SelectPercentile(f_regression,percentile=90)),
        ("model", XGBRegressor(random_state=7,learning_rate=0.05,max_depth=8,min_child_weight=1,
                               gamma=0.0,colsample_bytree=0.4)),
    ]
)

best_pipe_reg.fit(X_full,y_full_target)
Out[322]:
Pipeline(steps=[('CustomFeatures',
                 FunctionTransformer(func=<function custom_features at 0x0000022472FA8DC0>)),
                ('preprocessing',
                 ColumnTransformer(transformers=[('MinMaxScaler',
                                                  MinMaxScaler(),
                                                  ['budget', 'n_celebrities',
                                                   'runtime_budget_ratio',
                                                   'n_actors',
                                                   'n_production_companies']),
                                                 ('StandardScaler',
                                                  StandardScaler(),
                                                  ['runtime', 'n_genres']),
                                                 ('OneHot',
                                                  OneHotE...
                              gamma=0.0, gpu_id=-1, grow_policy='depthwise',
                              importance_type=None, interaction_constraints='',
                              learning_rate=0.05, max_bin=256,
                              max_cat_to_onehot=4, max_delta_step=0,
                              max_depth=8, max_leaves=0, min_child_weight=1,
                              missing=nan, monotone_constraints='()',
                              n_estimators=100, n_jobs=0, num_parallel_tree=1,
                              predictor='auto', random_state=7, reg_alpha=0,
                              reg_lambda=1, ...))])

Justificación para elección de mejor modelo de regresión para la competencia

Contemplando que la utilización de regresores mediante boosting con parámetros automáticos entregó mucho mejores resultados que los regresores más básicos, y con fines de optimización en el tiempo de ejecución, se decidió sólo hacer un gridsearch sobre los algoritmos más complejos, de esta forma la grilla contempla encontrar los mejores parámetros para los modelos de XGBoostRegressor y LightGBMRegressor. Cabe destacar que para este gridsearch, no se incluyeron los procesamientos mediante BoW o una grilla de percentiles, ya que se evalúan posteriormente.

Tras finalizar el procesamiento de la grilla, se encontró un conjunto de parámetros para el algoritmo de XGBRegressor que entregaban un R2 de 0.59 para el conjunto de entrenamiento, el cuál era levemente menor para el conjunto de test, alcanzando un 0.566.

Al elegir el modelo anteriormente descrito, se procede a realizar un fine-tunning con custom features para obtener un mejor desempeño, en este caso, a diferencia de los modelos de clasificación, el incluír todas las custom features creadas anteriormente, incluyendo los trigramas por BoW, mejoró levemente el desempeño del modelo, con un R2 de 0.567.

Considerando todo lo anterior, y de igual forma que para el clasificador, se procede a utilizar una base de datoas más grande para entrenar el modelo de regresión final para la predicción de la competencia, el que se confirmó que entregó mejores resultados.


6. Conclusiones¶

En conclusión, se pudieron cumplir todos los objetivos planteados, puesto que los modelos implementados, tanto de clasificación como de regresión, presentaron buen rendimiento con el conjunto de test para la competencia, los cuales fueron de $F1_{score}=0.96$ y $R2_{score}=0.89$, pese a haber obtenido desempeños regularmente bajos con el conjunto de validación.

Con respecto al análisis exploratorio de datos, se puede confirmar que éste fue sumamente importante para poder generar los features con los cuales finalmente se entrenaron los modelos. Por ejemplo, conocer las distribuciones de las variables numéricas, permitió saber que tipo de escalamiento de los datos fue el correcto a utilizar para cada una de las variables. Por otro lado, determinar el top 50 de productoras, artistas y keywords llevó a la creación de features importantes para definir si una película será exitosa o no, como los n_celebrities (cantidad de celbridades), in_top_production_companies (si tienen productoras en el top 50) y top_keyword (keyword perteneciente al top 50). En general, las CustomFeatures generadas fueron bastante significativas en el desempeño de los modelos, ya que al realizar el fine tuning de éstos se lograron obtener los mejores resultados.

En particular, con respecto a la clasificación, se tiene que incialmente con un modelo baseline se llegaron a valores de F1 score bajos, sin embargo, superiores a un modelo Dummy. Tras la optimización mediante un GridSearch no se logró mejorar mucho más el desempeño del baseline, por lo que fue necesario recurrir a algoritmos de XGBoost y LightGBM, de los cuales el primero presentó mejor desmempeño. Tras realizar el fine tuning por medio de GridSearch y de forma manual, se pasó de obtener para el conjunto de validación 0.29 de F1 score a 0.33 con el mejor modelo XGBoost.

Por otro lado, con respecto a la regresión, se tuvieron resultados similares a la clasificación, donde el modelo baseline (DecisionTreeRegressor) superó al Dummy, llegando a alcanzar un de R2 score de 0.305. Dado que los modelos de XGBoost y LightGBM presentaron mejores resultados en la tarea de clasificación, se optó por realizar únicamente un fine tuning a estos modelos, donde el mejor desempeño lo obtuvo el XGBRegressor con un valor de R2 score igual a 0.567, el cual inlcuyó todas las custom features creadas y los trigramas en base a Bag of Words, lo cual no se tuvo para el modelo de clasificación.

Cabe destacar que para los resultados de la competencia se optó por utilizar todos los datos entregados para entrenar ambos modelos, lo cual permitió incrementar los resultados de la competencia para ambas métricas y alcanzar valores más que satisfactorios.

Finalmente, algunas posibles formas de mejorar el rendimiento de los modelos corresponde a aplicar técnicas para mejorar el desbalance de las clases, como por ejemplo Under y OverSampling. Por otro lado, con respecto a los aprendizajes generales del proyecto, consideramos que realizar un buen análisis exploratorio de datos y un fine tuning de los modelos vía GridSearch, son estrategias sumamente importantes para obtener los mejores resultados posibles.



Anexo: Generación de Archivo Submit de la Competencia¶

Para subir los resultados obtenidos a la pagina de CodaLab utilice la función generateFiles entregada mas abajo. Esto es debido a que usted deberá generar archivos que respeten extrictamente el formato de CodaLab, de lo contario los resultados no se veran reflejados en la pagina de la competencia.

Para los resultados obtenidos en su modelo de clasificación y regresión, estos serán guardados en un archivo zip que contenga los archivos predicctions_clf.txt para la clasificación y predicctions_rgr.clf para la regresión. Los resultados, como se comento antes, deberan ser obtenidos en base al dataset test.pickle y en cada una de las lineas deberan presentar las predicciones realizadas.

Ejemplos de archivos:

  • [ ] predicctions_clf.txt

      Mostly Positive
      Mostly Positive
      Negative
      Positive
      Negative
      Positive
      ...
  • [ ] predicctions_rgr.txt

      16103.58
      16103.58
      16041.89
      9328.62
      107976.03
      194374.08
      ...
In [364]:
from zipfile import ZipFile
import os

def generateFiles(predict_data, clf_pipe, rgr_pipe):
    """Genera los archivos a subir en CodaLab

    Input
    predict_data: Dataframe con los datos de entrada a predecir
    clf_pipe: pipeline del clf
    rgr_pipe: pipeline del rgr

    Ouput
    archivo de txt
    """
    y_pred_clf = label_encoder.inverse_transform(clf_pipe.predict(predict_data))
    y_pred_rgr = rgr_pipe.predict(predict_data)

    with open('./predictions_clf.txt', 'w') as f:
        for item in y_pred_clf:
            f.write("%s\n" % item)

    with open('./predictions_rgr.txt', 'w') as f:
        for item in y_pred_rgr:
            f.write("%s\n" % item)

    with ZipFile('predictions.zip', 'w') as zipObj2:
       zipObj2.write('predictions_rgr.txt')
       zipObj2.write('predictions_clf.txt')

    os.remove("predictions_rgr.txt")
    os.remove("predictions_clf.txt")
In [365]:
predict_data = pd.read_pickle('test.pickle')
predict_data.tagline = predict_data.tagline.astype(str)
predict_data.overview = predict_data.overview.astype(str)
In [366]:
# Ejecutar función para generar el archivo de predicciones.
# perdict_data debe tener cargada los datos del text.pickle
# mientras que clf_pipe y rgr_pipe, son los pipeline de 
# clasificación y regresión respectivamente.
generateFiles(predict_data, best_pipe_cls,best_pipe_reg)
In [28]:
import os

os.system('jupyter nbconvert --to html Proyecto_enunciado.ipynb')
Out[28]:
0

Created in deepnote.com Created in Deepnote